package org.aner.tester.scanner;

import org.aner.tester.utils.text.StringUtils;
import org.apache.maven.project.MavenProject;
import org.junit.runners.Parameterized;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
import org.springframework.core.type.classreading.MetadataReader;
import org.springframework.core.type.classreading.MetadataReaderFactory;
import org.springframework.core.type.filter.TypeFilter;
import org.springframework.util.ClassUtils;
import org.springframework.util.SystemPropertyUtils;

import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.*;

/**
 * spring class scanner
 * @author aner
 * @version 1.0
 * @date 2020/10/19 21:17
 */

public class ClassScanner {

    Logger logger = LoggerFactory.getLogger(ClassScanner.class);
    /**
     * the file type
     */
    public static final String RESOURCE_PATTERN = "**/*.class";

    /**
     * path separator
     */
    private static final String PATH_SLASH = "/";

    /**
     * packages
     */
    private Set<String> classPackages;

    /**
     * filters set
     */
    private Set<TypeFilter> typeFilters;

    /**
     * filter type
     */
    private FilterFlag filterFlag;

    private ClassScanner() {
        classPackages = new HashSet<>();
        typeFilters = new HashSet<>();
    }

    /**
     * scan classes
     *
     * @return
     * @throws ClassNotFoundException
     * @throws IOException
     */
    public Map<String, Class<?>> scan() throws ClassNotFoundException, IOException {
        Map<String, Class<?>> classMap = new HashMap<>();
        if (StringUtils.isNotEmpty(classPackages)) {
            ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver(ProjectClassLoader.me().getClassLoader());
            MetadataReaderFactory readerFactory = new CachingMetadataReaderFactory(resourcePatternResolver);
            for (String pkg : this.classPackages) {
                String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX
                        + ClassUtils.convertClassNameToResourcePath(SystemPropertyUtils.resolvePlaceholders(pkg)) + PATH_SLASH
                        + RESOURCE_PATTERN;
                Resource[] resources = resourcePatternResolver.getResources(packageSearchPath);
                for (Resource resource : resources) {
                    if (resource.isReadable()) {
                        MetadataReader reader = readerFactory.getMetadataReader(resource);
                        String className = reader.getClassMetadata().getClassName();
                        if (matched(reader, readerFactory)) {
                            try {
                                classMap.put(className,ProjectClassLoader.me().getClassLoader().loadClass(className));
                            } catch (Error | Exception e) {
                                logger.error("Scanning {} from package, error: {}", className, e.getMessage());
                            }
                        }
                    }
                }
            }
        }
        return classMap;
    }

    /**
     * filter class
     *
     * @param reader
     * @param readerFactory
     * @return
     * @throws IOException
     */
    private boolean matched(MetadataReader reader, MetadataReaderFactory readerFactory) throws IOException {

        if (filterFlag == FilterFlag.ALL) {
            return filterAll(reader, readerFactory);
        } else if (filterFlag == FilterFlag.ONE){
            return filterWhether(reader, readerFactory);
        } else {
            return true;
        }
    }

    /**
     * all filters must be matched
     */
    private boolean filterAll(MetadataReader reader, MetadataReaderFactory readerFactory) throws IOException {
        if (StringUtils.isNotEmpty(typeFilters)) {
            for (TypeFilter filter : this.typeFilters) {
                if (!filter.match(reader, readerFactory)) {
                    return false;
                }
            }
            return true;
        }
        return false;
    }

    /**
     * any one of filters is matched
     */
    private boolean filterWhether(MetadataReader reader, MetadataReaderFactory readerFactory) throws IOException {
        if (StringUtils.isNotEmpty(typeFilters)) {
            for (TypeFilter filter : this.typeFilters) {
                if (filter.match(reader, readerFactory)) {
                    return true;
                }
            }
        }
        return false;
    }

    public static class Builder {

        private ClassScanner scanner = new ClassScanner();

        public Builder classPackage(String classPackage) {
            this.scanner.getClassPackages().add(classPackage);
            return this;
        }

        public Builder classPackages(String[] classPackages) {
            this.scanner.getClassPackages().addAll(Arrays.asList(classPackages));
            return this;
        }

        public Builder typeFilter(TypeFilter typeFilter) {
            this.scanner.getTypeFilters().add(typeFilter);
            return this;
        }

        public Builder typeFilters(List<TypeFilter> typeFilters) {
            this.scanner.getTypeFilters().addAll(typeFilters);
            return this;
        }

        public Builder filterFlag(FilterFlag filterFlag) {
            this.scanner.setFilterFlag(filterFlag);
            return this;
        }

        public ClassScanner build() {
            return this.scanner;
        }
    }

    /**
     * getter and setter
     */
    public Set<String> getClassPackages() {
        return classPackages;
    }

    public void setClassPackages(Set<String> classPackages) {
        this.classPackages = classPackages;
    }

    public Set<TypeFilter> getTypeFilters() {
        return typeFilters;
    }

    public void setTypeFilters(Set<TypeFilter> typeFilters) {
        this.typeFilters = typeFilters;
    }

    public FilterFlag getFilterFlag() {
        return filterFlag;
    }

    public void setFilterFlag(FilterFlag filterType) {
        this.filterFlag = filterType;
    }
}
